package com.lancoo.ccas52.arrangecourse.coursescheduling.campusscheduling;

import com.lancoo.ccas52.arrangecourse.common.ClassHourUtil;
import com.lancoo.ccas52.arrangecourse.coursescheduling.allocatetimeslotroom.AllocateNoClashTimeslotRoom;
import com.lancoo.ccas52.arrangecourse.coursescheduling.allocatetimeslotroom.AllocateTimeslotRoom;
import com.lancoo.ccas52.arrangecourse.coursescheduling.assignlist.SpecifiedTimelotRoom;
import com.lancoo.ccas52.arrangecourse.dataprocess.GenerateSchedulingData;
import com.lancoo.ccas52.arrangecourse.dataprocess.GenerateSplitClassGroup;
import com.lancoo.ccas52.arrangecourse.entities.ClassHour;
import com.lancoo.ccas52.arrangecourse.entities.CourseUnit;
import com.lancoo.ccas52.arrangecourse.entities.TeachingClassUnit;
import com.lancoo.ccas52.arrangecourse.entities.TimeslotRoom;
import com.lancoo.ccas52.arrangecourse.rulecheck.RuleInit;
import com.lancoo.ccas52.pojo.dto.ArrangeTeachingClassDto;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

/**
 * @Description 拆班教学班排课
 * @Author Fortysun
 * @Date 2023/8/23 14:03
 **/
@Slf4j
public class SplitTeachingClassScheduling extends GenerateSchedulingData {

    //定义分配教室课时分组的对象
    private SpecifiedTimelotRoom specifiedTimelotRoom;
    //定义拆班排课，教学班分组信息
    private GenerateSplitClassGroup splitClassGroup;

    public SplitTeachingClassScheduling(RuleInit ruleInit) {
        //初始化，按教室类型，处理教室课时分组信息列表的对象
        //初始化排课的对象
        if (specifiedTimelotRoom == null) {
            //初始化排课使用的教室课时分配的类，分配无冲突的教室课时分组信息；
            AllocateTimeslotRoom allocateNoClashTimeslotRoom = new AllocateNoClashTimeslotRoom();
            //初始化拆班排课，需要满足的规则
            specifiedTimelotRoom = new SpecifiedTimelotRoom(ruleInit.getNoClashBaseRule(), allocateNoClashTimeslotRoom);
        }

        if (splitClassGroup == null) {
            //初始化拆班分组教学班对象
            splitClassGroup = new GenerateSplitClassGroup();
        }
    }

    /**
     * 处理尽量排排课，一定排课的内容
     * @param taskId 排课计划id
     * @param teachingClassList 教学班列表
     * @param timeslotRoomList 教室课时分组列表
     * @param classHourMap 锁定的课表信息
     * @return
     */
    public LinkedList<TeachingClassUnit> start(Integer taskId,
                           List<ArrangeTeachingClassDto> teachingClassList,
                           LinkedList<TimeslotRoom> timeslotRoomList,
                           ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {
        //教学班列表不为空
        if (teachingClassList != null && !teachingClassList.isEmpty()) {
            if (timeslotRoomList == null || timeslotRoomList.isEmpty()) {
                log.error("SplitTeachingClassScheduling--start:没有用于排课的教室课时分组信息~！！");
                return null;
            }

            //筛选教学班列表中，需要拆班的教学班信息；
            List<ArrangeTeachingClassDto> splitTeachingClassList = findSplitTeachingClass(teachingClassList);

            if (splitTeachingClassList != null && !splitTeachingClassList.isEmpty()) {
                log.info("SplitTeachingClassScheduling--start:需要拆分的教学班的数量为："+
                        (splitTeachingClassList!=null?splitTeachingClassList.size():0));
                //记录，锁定的教室课时的数量，生成教学班的时候，用来作为教学班排课单元的起始值
                //这样，最后生成的排课单元的id，是连续的
                Integer size = ClassHourUtil.getMapElementSize(classHourMap);

                //将教学班信息，转换成排课的教学班单元
                LinkedList<TeachingClassUnit> teachingClassUnitList = convertToSplitTeachingClassUnit(taskId, splitTeachingClassList, size);
                log.info("SplitTeachingClassScheduling--start:转换为教学班排课单元的数量为："+
                        (teachingClassUnitList!=null?teachingClassUnitList.size():0));

                //将拆班教学班进行分组
                LinkedList<TeachingClassUnit> groupedList = splitClassGroup.start(teachingClassUnitList);
                //将教学班排课单元，按照分组标识，生成map，key为分组标识(UUID字符串)
                //其实就是将每个行政班生成的拆班教学班，按照分组标识，进行了分组
                //相同分组标识的教学班，需要分配相同课时的教室
                Map<String, LinkedList<TeachingClassUnit>> groupedUnitMap = groupedList.stream()
                        .collect(Collectors.groupingBy(TeachingClassUnit::getSplitClassGroupId, Collectors.toCollection(LinkedList::new)));

                //遍历待排教学班排课单元map
                for (Map.Entry<String, LinkedList<TeachingClassUnit>> entry : groupedUnitMap.entrySet()) {
                    //找出当前待分配的教学班排课单元列表信息，目前一个分组对应两个教学班排课单元
                    LinkedList<TeachingClassUnit> allocatedUnitList = entry.getValue();
                    //开始拆班教学班的排课
                    allocatedUnitList = startSplitScheduling(allocatedUnitList, timeslotRoomList, classHourMap);
                    //如果教室课时分组信息，分配成功
                    if (allocatedUnitList != null
                            && !allocatedUnitList.isEmpty()) {
                        //添加已排好的教室课时分组信息，到ClassHour
                        classHourMap = GenerateClashesClassHour(allocatedUnitList, classHourMap);
                        //从可分配的列表中，删除已分配的教室课时分组信息
                        removeTimeslotRoomList(timeslotRoomList, getUnitTimeslotList(allocatedUnitList));
                        //将当前教学班排课单元列表，从待分配的列表中移除
                        teachingClassUnitList.removeAll(allocatedUnitList);
                    }
                }

                log.info("SplitTeachingClassScheduling--start:剩余未分配的，要拆分的排课单元数量为："+
                        (teachingClassUnitList!=null?teachingClassUnitList.size():0));

                groupedUnitMap.clear();
                groupedUnitMap = null;
                return teachingClassUnitList;
            }
            log.info("SplitTeachingClassScheduling--start:没有需要拆分的教学班信息~！！");
        }
        log.info("SplitTeachingClassScheduling--start:没有需要处理的教学班信息~！！");

        return null;
    }



    /**
     * 获取同一拆班分组排课教学班单元分配的，教室课时分组信息列表信息
     * @param allocatedUnitList 同一分组的排课教学班单元列表信息
     * @return
     */
    private LinkedList<TimeslotRoom> getUnitTimeslotList(LinkedList<TeachingClassUnit> allocatedUnitList) {
        //定义返回的列表信息；
        LinkedList<TimeslotRoom> retList = new LinkedList<>();
        if (allocatedUnitList != null && !allocatedUnitList.isEmpty()) {
            for (TeachingClassUnit teachingClassUnit : allocatedUnitList) {
                //获取教学班排课单元分配的教室课时分组信息列表
                LinkedList<TimeslotRoom> timeslotRoomList = teachingClassUnit.getTimeslotRoomList();

                if (timeslotRoomList != null && !timeslotRoomList.isEmpty()) {
                    //分配给返回列表，进行返回
                    retList.addAll(timeslotRoomList);
                }
            }
        }
        return  retList;
    }

    /**
     * 生成无冲突的课表排课单元信息
     * @param teachingClassUnitList 已经分配的教学班排课单元列表
     * @param classHourMap 已锁定的课表信息
     */
    private ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> GenerateClashesClassHour(LinkedList<TeachingClassUnit> teachingClassUnitList,
                                          ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {
        if (teachingClassUnitList == null || teachingClassUnitList.isEmpty()) {
            log.error("SplitTeachingClassScheduling--GenerateClashesClassHour:待转换的教学班排课单元列表为空~！！");
            return classHourMap;
        }
        //生成教学班排课单元信息，并添加到map
        for (TeachingClassUnit teachingClassUnit: teachingClassUnitList) {
            for (TimeslotRoom timeslotRoom : teachingClassUnit.getTimeslotRoomList()) {
                //生成排课单元信息
                ClassHour classHour = new ClassHour();
                //为排课单元，添加教学班数据
                classHour.setCourseUnit(new CourseUnit(teachingClassUnit));
                //为排课单元，添加教室课时分组数据
                classHour.setTimeslotRoom(new TimeslotRoom(timeslotRoom));
                //获取分配的教室课时分组的，课时编码信息，如：0101代表周一第一节课，作为key，进行返回
                String key = timeslotRoom.getTimeslotCode();
                //将获取到的classHour存入map
                if (classHourMap.get(key) == null) {
                    //创建排课单元列表信息
                    CopyOnWriteArrayList<ClassHour> classHourList = new CopyOnWriteArrayList<>();
                    //将生成的教学班排课单元，添加到列表
                    classHourList.add(classHour);
                    //将列表添加到map中
                    classHourMap.put(key, classHourList);
                }
                else {
                    //直接将排课单元信息，添加到map里指定的列表中
                    classHourMap.get(key).add(classHour);
                }
            }
        }

        return classHourMap;
    }

    /**
     * 从教学班列表中，找出拆班的教学班信息
     * @param teachingClassList 教学班列表
     * @return
     */
    private List<ArrangeTeachingClassDto> findSplitTeachingClass(List<ArrangeTeachingClassDto> teachingClassList) {
        //用于保存教师优先排排课的教学班信息列表；
        List<ArrangeTeachingClassDto> splitTeachingClassList = new ArrayList<>();
        if (teachingClassList != null && !teachingClassList.isEmpty()) {
            //定义列表遍历的迭代器
            Iterator iterator = teachingClassList.iterator();
            while (iterator.hasNext()) {
                ArrangeTeachingClassDto teachingClass = (ArrangeTeachingClassDto)iterator.next();
                //判断如果是拆班的教学班,单双号标识不为空，并且指定了教室，说明是单双号拆班的教学班
                //单双号标识为空，说明是常规拆班的教学班
                if (StringUtils.isNotBlank(teachingClass.getUniqueShiftSign())
                        && teachingClass.getIsSingle() !=null
                        &&teachingClass.getRoomId()!= null) {
                    //不为空就将这个教学班加入到尽量排的教学班列表中
                    splitTeachingClassList.add(teachingClass);
                    //从原来的教学班中，将尽量排的教学班剔除，剩下的进行自动排课
                    iterator.remove();
                }
            }
        }

        return splitTeachingClassList;
    }

    /**
     * 开始拆班排课
     * @param teachingClassUnitList 待分配的拆班的教学班列表信息
     * @param timeslotRoomList 教室课时分组信息
     * @param classHourMap 当前已经排好的课表信息，用来进行冲突检测
     * @return
     */
    private LinkedList<TeachingClassUnit> startSplitScheduling(LinkedList<TeachingClassUnit>  teachingClassUnitList,
                                           LinkedList<TimeslotRoom> timeslotRoomList,
                                           ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {
        //将教室课时分组信息列表，以教室id为key，格式化保存为map
        Map<Long, LinkedList<TimeslotRoom>> roomIdRoomMap = timeslotRoomList.stream()
                .collect(Collectors.groupingBy(TimeslotRoom::getRoomId,Collectors.toCollection(LinkedList::new)));
        //定义待分配的List，用来保存筛选后的教室课时分组信息
        LinkedList<TimeslotRoom> allocatedList = new LinkedList<>();
        //筛选教学班排课单元，指定的教室对应的教室课时分组信息；
        for (TeachingClassUnit teachingClassUnit : teachingClassUnitList) {
            //获取教学班排课单元指定的教室id，多个以逗号分隔
            Long roomId = teachingClassUnit.getRoomId();
            LinkedList<TimeslotRoom> roomList = roomIdRoomMap.get(roomId);
            //如果某个教学班指定的教室，没有可分配的教室课时分组信息
            if (roomList == null) {
                log.error("#SplitTeachingClassScheduling.startSplitScheduling#:拆班教学班指定的ID为：" +roomId
                                +"的教室，没有可分配的教室课时分组信息~！！");
                //清空map
                roomIdRoomMap.clear();
                roomIdRoomMap = null;
                //直接返回空，说明此时这个教室不够分了，这两个拆分的教学班，
                //只能在自动排课的时候，分配在类型相同的其他教室了
                return null;
            }

            //将拆班课程，指定的教室的教室课时分组信息，进行添加
            allocatedList.addAll(roomList);
        }
        //将待分配的教室课时分组信息列表，按照课时编码，生成map
        Map<String, LinkedList<TimeslotRoom>> timeslotCodeMap = allocatedList.stream()
                .collect(Collectors.groupingBy(TimeslotRoom::getTimeslotCode, Collectors.toCollection(LinkedList::new)));

        //将教学班排课单元列表，转换成map，key为教室id
        //该操作主要是为了方便后面通过教室id，获取相应的教学班排课单元对象
        Map<Long, TeachingClassUnit> roomIdUnitMap = teachingClassUnitList.stream()
                .collect(Collectors.toMap(TeachingClassUnit::getRoomId, obj->obj, (key1, key2)->key1));

        //按照课时编码进行遍历，同时为教学班排课单元，指定可以分配的教室课时分组信息
        for (Map.Entry<String, LinkedList<TimeslotRoom>> entry : timeslotCodeMap.entrySet()) {
            //获取指定课时的，教室课时分组列表信息
            LinkedList<TimeslotRoom> timeslotCodeList = entry.getValue();
            //指定课时的教室，够分配，才进入
            if (timeslotCodeList.size() >= roomIdUnitMap.size()) {
                //按照课时进行教室课时分组信息的指定分配
                teachingClassUnitList = startAllocate(roomIdUnitMap, timeslotCodeList, classHourMap);
                //如果教师课时分组信息不够分配，将返回空
                if (teachingClassUnitList == null
                        ||teachingClassUnitList.isEmpty()) {
                    //此时直接向下一个课时进行遍历即可
                    continue;
                }
                roomIdRoomMap.clear();
                roomIdRoomMap = null;

                timeslotCodeMap.clear();
                timeslotCodeMap = null;

                //如果都分配成功了，直接返回
                return teachingClassUnitList;
            }
        }
        roomIdRoomMap.clear();
        roomIdRoomMap = null;

        timeslotCodeMap.clear();
        timeslotCodeMap = null;
        //遍历完所有的，都没有分配成功，那么返回空
        return null;
    }

    /**
     * 分配指定课时的教室课时分组信息，给同一分组的拆班教学班
     * @param roomIdUnitMap 教学班排课单元map，key为roomId
     * @param timeslotRoomList 待分配的教室课时分组信息列表
     * @param classHourMap 当前已经排好的课表信息，用来进行冲突检测
     * @return
     */
    private LinkedList<TeachingClassUnit> startAllocate(Map<Long, TeachingClassUnit> roomIdUnitMap ,
                                        LinkedList<TimeslotRoom> timeslotRoomList,
                                        ConcurrentHashMap<String, CopyOnWriteArrayList<ClassHour>> classHourMap) {
        LinkedList<TeachingClassUnit> retList= new LinkedList<>();
        //当列表不为空，且指定课时编码，所对应的教室课时分组的数量与教学班排课单元列表的数量相等才分配
        if (timeslotRoomList != null && !timeslotRoomList.isEmpty()) {
            //待分配的教室课时分组对象，需要转换成列表进行处理
            LinkedList<TimeslotRoom> allcatedList = new LinkedList();

            for (TimeslotRoom timeslotRoom : timeslotRoomList) {
                //加入之前，先清理之前的数据
                allcatedList.clear();
                //将合适的对象，加入处理的列表
                allcatedList.add(timeslotRoom);
                //获取指定教室id，对应的排课教学班单元
                TeachingClassUnit teachingClassUnit = roomIdUnitMap.get(timeslotRoom.getRoomId());
                //为指定排课教学班单元，分配教室课时分组信息；
                teachingClassUnit = specifiedTimelotRoom.ruleCheckAndAllocate(teachingClassUnit, allcatedList, classHourMap);

                //如果其中一个匹配不成功，直接跳出匹配循环
                if (teachingClassUnit.getTimeslotRoomList() == null) {
                    //分配不成功，将返回值列表置空~
                    retList = null;
                    //之前已经分配的，需要置空，进行回滚
                    roomIdUnitMap = rollbackAllocated(roomIdUnitMap, timeslotRoomList);

                    return null;
                }

                //将分配的分组信息，加入列表
                retList.add(teachingClassUnit);
            }
        }

        return retList;
    }

    /**
     * 指定的课时，不能将所有的课时对应的教室课时分组信息，分配给所有排课教学班单元对象，
     * 需要对数据进行回退
     * @param roomIdUnitMap 待分配的教学班排课单元
     * @param timeslotRoomList 已经分配的教室课时分组信息列表
     * @return
     */
    private Map<Long, TeachingClassUnit> rollbackAllocated(Map<Long, TeachingClassUnit> roomIdUnitMap,
                                                             LinkedList<TimeslotRoom> timeslotRoomList) {
        //遍历当前分配的教学班排课单元，查看分配的教室课时分组信息是否为空
        for (Map.Entry<Long, TeachingClassUnit> entry : roomIdUnitMap.entrySet()) {
            TeachingClassUnit teachingClassUnit = entry.getValue();

            //获取待分配的教室课时分组信息列表
            LinkedList<TimeslotRoom> allocatedRoomList = teachingClassUnit.getTimeslotRoomList();
            //如果分配失败了，将已分配的列表设为空
            if (allocatedRoomList != null&&!allocatedRoomList.isEmpty()) {
                //TODO 当前因为拆班教学班，连上节次都是1，所以列表元素始终是1，所以简单点，2023/9/19,by sunsl
                //TODO 如果后面换了场景，这个地方需要通过循环进行遍历处理
                TimeslotRoom backTimelotRoom = allocatedRoomList.get(0);
                //如果是完全匹配
                if (backTimelotRoom.getIsRemove()) {
                    //将之前分配的教室课时分组信息进行归还，如果不为空
                    timeslotRoomList.add(backTimelotRoom);
                }
                //将分配的列表置空
                allocatedRoomList.clear();
                //是否空间
                allocatedRoomList = null;
                //将以前分配的去掉，并设为空，下轮重新分配新的课时
                teachingClassUnit.setTimeslotRoomList(allocatedRoomList);
            }
        }

        return  roomIdUnitMap;
    }
}
